/*******************************************************\
|* Author: Trevor Sundberg
\*******************************************************/

// Includes
#include "SkugoFramework.hpp"
#include "SkugoBuffer.hpp"
#include "SkugoPacket.hpp"
#include <string>
#include <set>
#include <cmath>

namespace Skugo
{
  //***************************************************************************
  Framework* Framework::Instance = nullptr;
  const float Framework::TimeLimitPerUpdateMs = 2.0f;

  //***************************************************************************
  Framework::Framework() :
    IsLocal(false),
    CurrentAllowedTime(0.0f)
  {
    // Create the host
    this->Host = IHost::Create();
  }

  //***************************************************************************
  Framework* Framework::GetInstance()
  {
    return Instance;
  }
  
  //***************************************************************************
  bool StartsWith(string checkAgainst, string lookFor)
  {
    // If what we're checking against is smaller, then it cannot match
    if (checkAgainst.size() < lookFor.size())
    {
      return false;
    }

    size_t length = lookFor.size();

    for (size_t i = 0; i < length; ++i)
    {
      if (checkAgainst[i] != lookFor[i])
      {
        return false;
      }
    }

    return true;
  }

  //***************************************************************************
  void Framework::Start(string projectName, int argc, char* argv[], unsigned short recommendedPort)
  {
    // Error checking
    if (GetInstance() != nullptr)
    {
      SkugoError("The Skugo framework was already started, and cannot be started again");
    }

    // Create the framework
    Instance = new Framework();

    // We look for a special command line parameter that tells us if we're being launched by skugo
    const string LocalParameter = "-SkugoLocal:";
    
    // Loop through all the arguments that were passed in (skip our executable name)
    for (int i = 1; i < argc; ++i)
    {
      // If we found the local parameter...
      string parameter = argv[i];
      if (StartsWith(parameter, LocalParameter))
      {
        // We are running locally!
        Instance->IsLocal = true;

        // The local parameter should also have a port that we're using
        auto portText = parameter.substr(0, LocalParameter.size());
        
        // Try and parse the parameter port text into a number
        auto port = (unsigned short)atoi(portText.c_str());
        if (port != 0)
        {
          recommendedPort = port;
        }
      }
    }
    
    // Finally, set the project name and start hosting
    Instance->ProjectName = projectName;
    Instance->Host->StartHosting(recommendedPort);
  }
  
  //***************************************************************************
  void Framework::Stop()
  {
    // Shutdown the host, which kills all client connections
    this->Host->Shutdown();

    // Update one last time so that all clients will be properly disposed of
    // The connection for each client will be tested, and it will cause the clients to be removed
    this->Update();

    // Clear the instance of the framework
    Instance = nullptr;
  }
  
  //***************************************************************************
  void Framework::Update()
  {
    //// Create a stopwatch to balance out time
    //Stopwatch timeLeft = new Stopwatch();
    //timeLeft.Start();

    //// Clone the list of event handlers
    //var toBeUpdated = this.ToBeUpdated.ToList();

    //// Add to the allowed amount of time
    //this.CurrentAllowedTime += this.TimeLimitPerUpdateMs;

    //// Never go over the allowed amount (we roll over negatives, not positives!)
    //if (this.CurrentAllowedTime > this.TimeLimitPerUpdateMs)
    //{
    //    this.CurrentAllowedTime = this.TimeLimitPerUpdateMs;
    //}

    //// While we still have time...
    //while (toBeUpdated.Count != 0)
    //{
    //    // Get the time ran in milliseconds
    //    var timeMs = (Single)timeLeft.Elapsed.TotalMilliseconds;

    //    // If we go outside the time limit...
    //    if (timeMs > this.CurrentAllowedTime)
    //    {
    //        // Stop here!
    //        timeLeft.Stop();
    //        break;
    //    }

    //    // Generate an index for an update function
    //    Int32 index = this.Rand.Next(toBeUpdated.Count);

    //    // Grab a random update callback
    //    var update = toBeUpdated[index];

    //    // Swap-erase the index
    //    Int32 lastIndex = toBeUpdated.Count - 1;
    //    toBeUpdated[index] = toBeUpdated[lastIndex];
    //    toBeUpdated.RemoveAt(lastIndex);

    //    // Invoke the update event
    //    update();
    //}

    //// Subtract off the amount of time spent
    //this.CurrentAllowedTime -= (Single)timeLeft.Elapsed.TotalMilliseconds;

    // Attempt to pop a new connection from the host
    while (IConnection* newConnection = this->Host->PopNewConnection())
    {
      // Create a new client
      auto newClient = new Client(newConnection);

      // Create a client and add it to the list
      this->Clients.push_back(newClient);

      // Tell the client the project name
      this->SendProjectName(newClient);
      
      // Send an event that a client joined
      this->OnClientConnected.Invoke(newClient);
    }

    // Loop through all the clients and process incoming packets
    for (sizeType i = 0; i < this->Clients.size();)
    {
      // Get the current client
      auto client = this->Clients[i];

      // If the client ever disconnects...
      if (client->Connection->IsConnected() == false)
      {
        // Send an event that a client joined
        this->OnClientDisconnected.Invoke(client);

        // Destroy all of the plugins for that client
        client->LocalPlugins->DestroyAllPlugins();

        // Remove the current client (which will shift everything in front)
        this->Clients.erase(this->Clients.begin() + i);

        // If we were running locally, and the client that just disconnected was our local client...
        if (this->IsLocal && client->IsLocal())
        {
            // Kill the current process
            exit(0);
            return;
        }
      }
      else
      {
        // Process all the incoming packets to this client
        client->Router->ProcessAllIncoming();

        // Increment to the next client
        ++i;
      }
    }
  }
  
  //***************************************************************************
  void Framework::SendProjectName(Client* client)
  {
    // Write out the packet header
    Buffer outgoing;
    outgoing.Write<unsigned>(sizeof(unsigned) + this->ProjectName.size());
    outgoing.Write<unsigned char>(PacketType::Initialize);

    // All of our packets currently require an instance guid
    // This packet type does not, but I'd rather not change it everywhere
    // I'm basically writing out a dummy instance guid
    outgoing.Write<Guid>(-1);

    // Write the provided data
    outgoing.WriteAsciiString(this->ProjectName);

    // Send the packet out
    client->Connection->Write(outgoing.GetData(), outgoing.GetSize());
  }
  
  //***************************************************************************
  Client::Client(IConnection* connection) :
    Connection(connection),
    LocalPlugins(new PluginContainer()),
    Router(new PacketRouter(connection, LocalPlugins))
  {
  }

  //***************************************************************************
  bool Client::IsLocal()
  {
    return this->Connection->IsLocal();
  }
}
